// ==UserScript==
// @name         七牛云带宽压测与节点别名显示 (Fully Aligned)
// @namespace    Violentmonkey Scripts
// @match        https://portal.niulinkcloud.com/node/node-operation/nodes/all*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @version      3.0
// @author       - (with Gemini modifications)
// @description  在七牛云节点管理页面，添加“节点别名”和“宽带备注”列，并显示最近4次带宽压测达标率。通过智能的防抖动同步机制，完美解决初始加载时高度不对齐及鼠标悬浮抖动的问题。
// @homepageURL  https://github.com/gemini-script-generator/niulink-bandwidth-script
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const originalBandwidthHeader = "带宽压测总结";
    const bandwidthHeaderTarget = "节点矿主";
    const nodeAliasHeader = "节点别名";
    const bandwidthRemarkHeader = "宽带备注";
    const referenceHeader = "节点备注";
    const remarkTruncateLength = 20;
    const apiEndpoint = "http://211.154.24.22:8080/devices/";
    const apiToken = "wangmai888";
    const DEBOUNCE_DELAY = 150; // ms, 防抖延迟
    // --- END CONFIGURATION ---

    let bandwidthColumnIndex = -1;
    let remarkColumnIndex = -1;
    let areHeadersModified = false;

    // Inject styles
    GM_addStyle(`
        .gm-row-synced > td { height: inherit !important; }
        .gm-popup-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; }
        .gm-popup-content { background: #fff; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 5px 15px rgba(0,0,0,0.3); position: relative; }
        .gm-popup-close { position: absolute; top: 10px; right: 10px; border: none; background: transparent; font-size: 24px; cursor: pointer; }
        .gm-popup-text { font-family: monospace; white-space: pre-wrap; word-break: break-all; max-height: 60vh; overflow-y: auto; }
    `);

    function debounce(func, delay) {
        let timeout;
        return function(...args) {
            const context = this;
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(context, args), delay);
        };
    }

    /**
     * A robust function to sync row heights across main and fixed columns.
     * This uses a double requestAnimationFrame to ensure calculations
     * happen after the browser has completed its layout pass.
     */
    function syncRowHeights() {
        const mainTableBody = document.querySelector('.ant-table-scroll .ant-table-tbody');
        if (!mainTableBody) return;

        const mainRows = Array.from(mainTableBody.querySelectorAll(':scope > tr.ant-table-row'));
        const fixedLeftRows = Array.from(document.querySelectorAll('.ant-table-fixed-left .ant-table-tbody > tr.ant-table-row'));
        const fixedRightRows = Array.from(document.querySelectorAll('.ant-table-fixed-right .ant-table-tbody > tr.ant-table-row'));

        if (mainRows.length === 0) return;

        mainRows.forEach((row, index) => {
            row.classList.remove('gm-row-synced');
            row.style.height = 'auto';
            if (fixedLeftRows[index]) {
                fixedLeftRows[index].classList.remove('gm-row-synced');
                fixedLeftRows[index].style.height = 'auto';
            }
            if (fixedRightRows[index]) {
                fixedRightRows[index].classList.remove('gm-row-synced');
                fixedRightRows[index].style.height = 'auto';
            }
        });

        requestAnimationFrame(() => {
            requestAnimationFrame(() => {
                mainRows.forEach((mainRow, index) => {
                    const leftRow = fixedLeftRows[index];
                    const rightRow = fixedRightRows[index];

                    if (!mainRow.offsetParent) return;

                    const maxHeight = Math.max(
                        mainRow.offsetHeight,
                        leftRow ? leftRow.offsetHeight : 0,
                        rightRow ? rightRow.offsetHeight : 0
                    );

                    if (maxHeight > 0) {
                        mainRow.style.height = `${maxHeight}px`;
                        mainRow.classList.add('gm-row-synced');
                        if (leftRow) {
                            leftRow.style.height = `${maxHeight}px`;
                            leftRow.classList.add('gm-row-synced');
                        }
                        if (rightRow) {
                            rightRow.style.height = `${maxHeight}px`;
                            rightRow.classList.add('gm-row-synced');
                        }
                    }
                });
            });
        });
    }

    // *** FIX: Create a debounced version of the sync function. ***
    // We can call this many times, but it will only run once after the last call.
    const debouncedSyncRowHeights = debounce(syncRowHeights, DEBOUNCE_DELAY);


    function showInfoPopup(text) {
        const overlay = document.createElement('div');
        overlay.className = 'gm-popup-overlay';
        overlay.innerHTML = `
            <div class="gm-popup-content">
                <button class="gm-popup-close" title="Close">&times;</button>
                <pre class="gm-popup-text"></pre>
            </div>
        `;
        overlay.querySelector('.gm-popup-text').textContent = text;
        const close = () => document.body.removeChild(overlay);
        overlay.querySelector('.gm-popup-close').onclick = close;
        overlay.onclick = (e) => {
            if (e.target === overlay) close();
        };
        document.body.appendChild(overlay);
    }

    async function fetchAndDisplayRates(nodeId, targetCell) {
        if (!nodeId || !targetCell) return;
        try {
            targetCell.textContent = '加载中...';
            const response = await fetch(`https://portal.niulinkcloud.com/api/proxy/jarvis/v1/nodes/${nodeId}/pressurereport?size=10`);
            if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
            const data = await response.json();
            if (data && Array.isArray(data.pressure) && data.pressure.length > 0) {
                const rates = data.pressure
                    .sort((a, b) => b.pressureTime - a.pressureTime)
                    .slice(0, 4)
                    .map(p => Math.trunc(p.summary.limitBwAchieveRate))
                    .join(', ');
                targetCell.textContent = rates || '无数据';
            } else {
                targetCell.textContent = '无数据';
            }
        } catch (error) {
            console.error(`[GM Script] Error fetching bandwidth for ${nodeId}:`, error);
            targetCell.textContent = '获取失败';
        } finally {
            // *** FIX: Trigger a sync after the cell content is updated. ***
            debouncedSyncRowHeights();
        }
    }

    function fetchAndDisplayNodeDetails(nodeId, aliasCell, remarkCell) {
        if (!nodeId || !aliasCell || !remarkCell) return;
        aliasCell.textContent = '...';
        remarkCell.textContent = '...';
        GM_xmlhttpRequest({
            method: "GET",
            url: `${apiEndpoint}${nodeId}`,
            headers: { "X-Token": apiToken, "Accept": "application/json" },
            onload: function(response) {
                try {
                    if (response.status >= 200 && response.status < 300) {
                        const data = JSON.parse(response.responseText);
                        aliasCell.textContent = data.name || '无名称';
                        const remark = data.mgt_rmk || '无备注';
                        if (remark.length > remarkTruncateLength) {
                            remarkCell.innerHTML = `
                                <span style="vertical-align: middle;">${remark.substring(0, remarkTruncateLength)}...</span>
                                <svg width="1em" height="1em" viewBox="0 0 16 16" style="margin-left: 5px; cursor: pointer; vertical-align: middle;">
                                    <path fill="currentColor" d="M8,1 C11.866,1 15,4.134 15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 Z M8,2.5 C4.962,2.5 2.5,4.962 2.5,8 C2.5,11.038 4.962,13.5 8,13.5 C11.038,13.5 13.5,11.038 13.5,8 C13.5,4.962 11.038,2.5 8,2.5 Z M8.75,7 L8.75,12 L7.25,12 L7.25,7 L8.75,7 Z M8.75,4 L8.75,6 L7.25,6 L7.25,4 L8.75,4 Z"></path>
                                </svg>
                            `;
                            remarkCell.querySelector('svg').addEventListener('click', (e) => {
                                e.stopPropagation();
                                showInfoPopup(remark);
                            });
                        } else {
                            remarkCell.textContent = remark;
                        }
                    } else if (response.status === 404) {
                        aliasCell.textContent = '无记录';
                        remarkCell.textContent = '无记录';
                    } else {
                        aliasCell.textContent = '获取失败';
                        remarkCell.textContent = '获取失败';
                    }
                } catch (e) {
                    console.error("[GM Script] Error parsing node details:", e);
                    aliasCell.textContent = '解析错误';
                    remarkCell.textContent = '解析错误';
                } finally {
                    // *** FIX: Trigger a sync after the cell content is updated. ***
                    debouncedSyncRowHeights();
                }
            },
            onerror: function(error) {
                console.error(`[GM Script] Error fetching details for ${nodeId}:`, error);
                aliasCell.textContent = '网络错误';
                remarkCell.textContent = '网络错误';
                // *** FIX: Trigger a sync even on error to ensure alignment. ***
                debouncedSyncRowHeights();
            }
        });
    }

    function processNewRows() {
        if (!areHeadersModified) {
            const headers = document.querySelectorAll('.ant-table-thead th');
            if (headers.length === 0) return false;

            let tempBandwidthIndex = -1, tempRemarkIndex = -1;
            headers.forEach((h, i) => {
                const titleElement = h.querySelector('.ant-table-column-title');
                if (titleElement) {
                    if (titleElement.textContent.includes(bandwidthHeaderTarget)) tempBandwidthIndex = i;
                    if (titleElement.textContent.includes(referenceHeader)) tempRemarkIndex = i;
                }
            });

            if (tempBandwidthIndex !== -1 && tempRemarkIndex !== -1) {
                bandwidthColumnIndex = tempBandwidthIndex;
                remarkColumnIndex = tempRemarkIndex;

                document.querySelectorAll('.ant-table-thead').forEach(thead => {
                    const currentHeaders = thead.querySelectorAll('th');
                    const bandwidthHeaderTitle = currentHeaders[bandwidthColumnIndex]?.querySelector('.ant-table-column-title');
                    if (bandwidthHeaderTitle) bandwidthHeaderTitle.textContent = originalBandwidthHeader;

                    const referenceNode = currentHeaders[remarkColumnIndex];
                    if (referenceNode && !referenceNode.nextElementSibling?.textContent.includes(nodeAliasHeader)) {
                         const aliasTh = document.createElement('th');
                         aliasTh.className = referenceNode.className;
                         aliasTh.innerHTML = `<span class="ant-table-header-column"><div><span class="ant-table-column-title">${nodeAliasHeader}</span></div></span>`;

                         const remarkTh = document.createElement('th');
                         remarkTh.className = referenceNode.className;
                         remarkTh.innerHTML = `<span class="ant-table-header-column"><div><span class="ant-table-column-title">${bandwidthRemarkHeader}</span></div></span>`;

                         referenceNode.after(remarkTh);
                         referenceNode.after(aliasTh);
                    }
                });
                areHeadersModified = true;
            }
        }

        if (bandwidthColumnIndex === -1 || remarkColumnIndex === -1) return false;

        const rowsToProcess = document.querySelectorAll('.ant-table-scroll .ant-table-tbody > tr.ant-table-row:not([data-gm-processed])');

        if (rowsToProcess.length === 0) {
            return false;
        }

        rowsToProcess.forEach(row => {
            row.setAttribute('data-gm-processed', 'true');
            const nodeId = row.getAttribute('data-row-key');
            const bandwidthCell = row.cells[bandwidthColumnIndex];

            const aliasTd = row.insertCell(remarkColumnIndex + 1);
            const remarkTd = row.insertCell(remarkColumnIndex + 2);
            aliasTd.className = row.cells[remarkColumnIndex].className;
            remarkTd.className = row.cells[remarkColumnIndex].className;

            if (nodeId && bandwidthCell) {
                fetchAndDisplayRates(nodeId, bandwidthCell);
                fetchAndDisplayNodeDetails(nodeId, aliasTd, remarkTd);
            }
        });

        return true;
    }

    function handleTableChange() {
        processNewRows();
        // The individual fetch functions will now handle triggering the sync,
        // so we no longer need to call it here. This avoids the jitter on hover.
    }

    // --- SCRIPT EXECUTION ---
    const smartTableChangeHandler = debounce(handleTableChange, DEBOUNCE_DELAY);

    const observer = new MutationObserver(smartTableChangeHandler);

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Resize still needs to trigger the debounced sync directly.
    window.addEventListener('resize', debouncedSyncRowHeights);

    // Initial run to start the process.
    setTimeout(handleTableChange, 1500);
})();
